<!DOCTYPE html>
<html>
  <head>
    <!-- Notes: This should be open in its original path -->
    <meta charset="utf-8" />
    <link rel="stylesheet" href="../script/semantic/semantic.min.css" />
    <script src="../script/jquery-3.6.0.min.js"></script>
    <script src="../script/semantic/semantic.min.js"></script>
  </head>
  <body>
    <link rel="stylesheet" href="../darktheme.css" />
    <script src="../script/darktheme.js"></script>
    <br />
    <div class="ui container">
      <div class="ui form">
        <div class="field">
          <input
            id="searchbar"
            type="text"
            placeholder="Search Containers ..."
            autocomplete="off"
          />
        </div>
        <div class="field">
          <div class="ui checkbox">
            <input type="checkbox" id="showOnlyRunning" class="hidden" />
            <label for="showOnlyRunning">Show Only Running Containers</label>
          </div>
        </div>
        <div class="field">
          <div class="ui checkbox">
            <input type="checkbox" id="showUnexposed" class="hidden" />
            <label for="showUnexposed"
              >Show Containers with Unexposed Ports</label
            >
          </div>
        </div>
      </div>
      <!--  Networked Containers Lists -->
      <div class="ui header">
        <div class="content">
          Containers on Zoraxy's Networks
          <div class="sub header">
            These containers share a network with Zoraxy.<br />
            Your networks must support Docker DNS-based name resolution.
          </div>
        </div>
      </div>
      <div id="networkedList" class="ui middle aligned divided list">
        <div class="ui active loader"></div>
      </div>
      <div class="ui horizontal divider"></div>
      <!-- Host Mode Containers List -->
      <div id="hostmodeListHeader" class="ui header" hidden>
        <div class="content">
          Containers using Host Network
          <div class="sub header">
            These containers use the host network configuration.<br />
            Ports must be manually configured.
          </div>
        </div>
      </div>
      <div id="hostmodeList" class="ui middle aligned divided list"></div>
      <div class="ui horizontal divider"></div>
      <!-- Other Containers List -->
      <div id="othersListHeader" class="ui header" hidden>
        <div class="content">
          Containers on different Networks
          <div class="sub header">
            These containers are not connected to Zoraxy's networks.<br />
            Manual configuration is required.
          </div>
        </div>
      </div>
      <div id="othersList" class="ui middle aligned divided list"></div>
      <div class="ui horizontal divider"></div>
      <!-- Existing List -->
      <div id="existingListHeader" class="ui header" hidden>
        Containers with existing Proxy Rules
        <div class="sub header">
          These containers are already configured in the proxy rules.
        </div>
      </div>
      <div id="existingList" class="ui middle aligned divided list"></div>
    </div>
    <script>
      // DOM elements
      const $networkedList = $("#networkedList");

      const $hostmodeListHeader = $("#hostmodeListHeader");
      const $hostmodeList = $("#hostmodeList");

      const $othersListHeader = $("#othersListHeader");
      const $othersList = $("#othersList");

      const $existingListHeader = $("#existingListHeader");
      const $existingList = $("#existingList");

      const $searchbar = $("#searchbar");
      const $showOnlyRunning = $("#showOnlyRunning");
      const $showUnexposed = $("#showUnexposed");

      // maps for containers
      let networkedEntries = {};
      let hostmodeEntries = {};
      let othersEntries = {};
      let existingEntries = {};

      // initial load
      $(document).ready(() => {
        loadCheckboxState("showUnexposed", $showUnexposed);
        loadCheckboxState("showOnlyRunning", $showOnlyRunning);
        initializeEventListeners();
        getDockerContainers();
      });

      // event listeners
      function initializeEventListeners() {
        $showUnexposed.on("change", () => {
          saveCheckboxState("showUnexposed", $showUnexposed);
          getDockerContainers();
        });

        $showOnlyRunning.on("change", () => {
          saveCheckboxState("showOnlyRunning", $showOnlyRunning);
          getDockerContainers();
        });

        // debounce searchbar input to prevent excessive filtering
        $searchbar.on(
          "input",
          debounce(() => filterLists($searchbar.val().toLowerCase()), 300)
        );

        $networkedList.on("click", ".add-button", (event) => {
          const key = $(event.currentTarget).data("key");
          if (networkedEntries[key]) {
            parent.addContainerItem(networkedEntries[key]);
          }
        });

        $hostmodeList.on("click", ".add-button", (event) => {
          const key = $(event.currentTarget).data("key");
          if (hostmodeEntries[key]) {
            parent.addContainerItem(hostmodeEntries[key]);
          }
        });
      }
      // filter lists by toggling item visibility
      function filterLists(searchTerm) {
        $(".list .item").each((_, item) => {
          const content = $(item).text().toLowerCase();
          $(item).toggle(content.includes(searchTerm));
        });
      }

      // reset UI and state
      function reset() {
        networkedEntries = {};
        hostmodeEntries = {};
        othersEntries = {};
        existingEntries = {};

        $networkedList.empty();
        $hostmodeList.empty();
        $othersList.empty();
        $existingList.empty();

        $hostmodeListHeader.attr("hidden", true);
        $othersListHeader.attr("hidden", true);
        $existingListHeader.attr("hidden", true);
      }

      // process docker data
      async function getDockerContainers() {
        reset();
        $networkedList.html('<div class="ui active loader"></div>');

        try {
          const [hostData, dockerData] = await Promise.all([
            $.get("/api/proxy/list?type=host"),
            $.get("/api/docker/containers"),
          ]);
          if (!hostData.error && !dockerData.error) {
            processDockerData(hostData, dockerData);
          } else {
            showError(hostData.error || dockerData.error);
          }
        } catch (error) {
          console.error(error);
          parent.msgbox("Error loading data: " + error.message, false);
        }
      }

      function processDockerData(hostData, dockerData) {
        const { containers } = dockerData;
        const existingTargets = new Set(
          hostData.flatMap(({ ActiveOrigins }) =>
            ActiveOrigins.map(({ OriginIpOrDomain }) => OriginIpOrDomain)
          )
        );

        // identify the Zoraxy container to determine shared networks
        const zoraxyContainer = containers.find(
          (container) =>
            container.Labels &&
            container.Labels["com.imuslab.zoraxy.container-identifier"] ===
              "Zoraxy"
        );

        const zoraxyNetworkIDs = zoraxyContainer
          ? Object.values(zoraxyContainer.NetworkSettings.Networks).map(
              (network) => network.NetworkID
            )
          : [];

        // iterate over all containers
        containers.forEach((container) => {
          // skip containers in network mode "none"
          if (container.HostConfig.NetworkMode === "none") {
            return;
          }

          // skip containers not running, if the option is enabled
          if (
            container.State !== "running" &&
            $showOnlyRunning.prop("checked")
          ) {
            return;
          }

          // sanitize container name
          const containerName = container.Names[0].replace(/^\//, "");

          // containers in network mode "host" should resolve to "host.docker.internal"
          if (
            container.HostConfig.NetworkMode === "host" &&
            !hostmodeEntries[container.Id]
          ) {
            hostmodeEntries[container.Id] = {
              name: containerName,
              ip: "host.docker.internal",
            };
            return;
          }

          // networks that are shared with Zoraxy
          const sharedNetworks = Object.values(
            container.NetworkSettings.Networks
          ).filter((network) => zoraxyNetworkIDs.includes(network.NetworkID));

          if (!sharedNetworks.length) {
            const ips = Object.values(container.NetworkSettings.Networks).map(
              (network) => network.IPAddress
            );

            const ports = container.Ports.map((portObject) => {
              return portObject.PublicPort || portObject.PrivatePort;
            });

            othersEntries[container.Id] = {
              name: containerName,
              ips,
              ports,
            };
            return;
          }

          // add the container to the networked list, using it's name as address
          container.Ports.forEach((portObject) => {
            const port = portObject.PublicPort || portObject.PrivatePort;
            const key = `${containerName}:${port}`;

            // always include existing proxy-rule targets
            if (existingTargets.has(key)) {
              if (!existingEntries[key]) {
                existingEntries[key] = {
                  name: containerName,
                  ip: containerName,
                  port,
                };
              }
            }
            // otherwise, include only if exposed or checkbox is checked
            else if (portObject.PublicPort || $showUnexposed.is(":checked")) {
              if (!networkedEntries[key]) {
                networkedEntries[key] = {
                  name: containerName,
                  ip: containerName,
                  port,
                };
              }
            }
          });
        });

        // finally update the UI
        updateNetworkedList();
        updateHostmodeList();
        updateOthersList();
        updateExistingList();
      }

      // update networked list
      function updateNetworkedList() {
        $networkedList.empty();
        let html = "";
        Object.entries(networkedEntries)
          .sort()
          .forEach(([key, entry]) => {
            html += `
              <div class="item">
                <div class="content" style="display: flex; justify-content: space-between;">
                  <div>
                    <div class="header">${entry.name}</div>
                    <div class="description">
                      <p>${entry.ip}:${entry.port}</p>
                    </div>
                  </div>
                  <button class="ui button add-button" data-key="${key}">
                    Add
                  </button>
                </div>
              </div>
            `;
          });
        $networkedList.append(html);
      }

      // update hostmode list
      function updateHostmodeList() {
        $hostmodeList.empty();
        let html = "";
        Object.entries(hostmodeEntries)
          .sort()
          .forEach(([key, entry]) => {
            html += `
              <div class="item">
                <div class="content" style="display: flex; justify-content: space-between;">
                  <div>
                    <div class="header">${entry.name}</div>
                    <div class="description">
                      <p>${entry.ip}</p>
                    </div>
                  </div>
                  <button class="ui right floated button add-button" data-key="${key}">
                    Add
                  </button>
                </div>
              </div>
            `;
          });
        $hostmodeList.append(html);
        if (Object.keys(hostmodeEntries).length) {
          $hostmodeListHeader.removeAttr("hidden");
        }
      }

      // update others list
      function updateOthersList() {
        $othersList.empty();
        let html = "";
        Object.entries(othersEntries)
          .sort()
          .forEach(([key, entry]) => {
            html += `
              <div class="item">
                <div class="header">${entry.name}</div>
                ${
                  entry.ips.length === 0 ||
                  entry.ips.every((ip) => ip === "") ||
                  entry.ports.length === 0 ||
                  entry.ports.every((port) => port === "")
                    ? `<div class="description">
                        <p>No IPs or Ports</p>
                      </div>`
                    : `<div class="description">
                      <p>
                        IPs: ${entry.ips.join(", ")}<br />
                        Ports: ${entry.ports.join(", ")}
                      </p>
                    </div>`
                }
              </div>
            `;
          });
        $othersList.append(html);
        if (Object.keys(othersEntries).length) {
          $othersListHeader.removeAttr("hidden");
        }
      }

      // update existing rules list
      function updateExistingList() {
        $existingList.empty();
        let html = "";
        Object.entries(existingEntries)
          .sort()
          .forEach(([key, entry]) => {
            html += `
              <div class="item">
                <div class="content">
                  <div class="header">${entry.name}</div>
                  <div class="description">
                    <p>${entry.ip}:${entry.port}</p>
                  </div>
                </div>
              </div>
            `;
          });
        $existingList.append(html);
        if (Object.keys(existingEntries).length) {
          $existingListHeader.removeAttr("hidden");
        }
      }

      // show error message
      function showError(error) {
        $networkedList.html(
          `<div class="ui basic segment"><i class="ui red times icon"></i> ${error}</div>`
        );
        parent.msgbox(`Error loading data: ${error}`, false);
      }

      //
      // utils
      //

      // local storage handling
      function loadCheckboxState(id, $elem) {
        const state = localStorage.getItem(id);
        if (state !== null) {
          $elem.prop("checked", state === "true");
        }
      }

      function saveCheckboxState(id, $elem) {
        localStorage.setItem(id, $elem.prop("checked"));
      }

      // debounce function
      function debounce(func, delay) {
        let timeout;
        return (...args) => {
          clearTimeout(timeout);
          timeout = setTimeout(() => func(...args), delay);
        };
      }
    </script>
  </body>
</html>
